package com.fscp.seckill.service;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.IdUtil;
import com.fscp.commons.constant.ApiConstant;
import com.fscp.commons.constant.RedisKeyConstant;
import com.fscp.commons.exception.ParameterException;
import com.fscp.commons.model.domain.ResultInfo;
import com.fscp.commons.model.pojo.SeckillVoucher;
import com.fscp.commons.model.pojo.VoucherOrder;
import com.fscp.commons.model.vo.SignInDinerInfo;
import com.fscp.commons.utils.AssertUtil;
import com.fscp.commons.utils.ResultInfoUtil;
import com.fscp.seckill.mapper.SeckillVoucherMapper;
import com.fscp.seckill.mapper.VoucherOrderMapper;
import com.fscp.seckill.model.RedisLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * 秒杀业务逻辑层
 */
@Service
public class SeckillService {
    
    @Resource
    private SeckillVoucherMapper seckillVoucherMapper;
    @Resource
    private VoucherOrderMapper voucherOrderMapper;
    @Resource
    private RestTemplate restTemplate;
    @Value("${service.name.ms-oauth-server}")
    private String oauthServerName;
    @Resource
    private RedisTemplate redisTemplate;
    @Resource
    private DefaultRedisScript defaultRedisScript;
    @Resource
    private RedisLock redisLock;
    @Resource
    private RedissonClient redissonClient;

    /**
     * 添加需要抢购的代金券
     *
     * @param seckillVoucher
     */
    @Transactional(rollbackFor = Exception.class)
    public void addSeckillVoucher(SeckillVoucher seckillVoucher) {
        // 健壮性处理

        // 非空校验
        AssertUtil.isTrue(seckillVoucher.getFkVoucherId() == null, "请选择需要抢购的代金券");
        AssertUtil.isTrue(seckillVoucher.getAmount() == 0, "请输入抢购总数量");
        Date now = new Date();
        AssertUtil.isNotNull(seckillVoucher.getStartTime(), "请输入开始时间");
        // 生产环境下面一行代码需放行，这里注释方便测试
        // AssertUtil.isTrue(now.after(seckillVoucher.getStartTime()), "开始时间不能早于当前时间");
        AssertUtil.isNotNull(seckillVoucher.getEndTime(), "请输入结束时间");
        AssertUtil.isTrue(now.after(seckillVoucher.getEndTime()), "结束时间不能早于当前时间");
        AssertUtil.isTrue(seckillVoucher.getStartTime().after(seckillVoucher.getEndTime()), "开始时间不能晚于结束时间");

        /* 注释原始的走 关系型数据库 的流程

        // 验证数据库中是否已经存在该券的秒杀活动
        SeckillVoucher seckillVoucherFromDb = seckillVoucherMapper.selectSeckillVoucher(seckillVoucher.getFkVoucherId());
        AssertUtil.isTrue(seckillVoucherFromDb != null, "该券已经拥有了抢购活动");

        // 插入数据库
        seckillVoucherMapper.save(seckillVoucher);

        */

        // 采用 Redis 实现
        String key = RedisKeyConstant.seckill_voucher.getKey() + seckillVoucher.getFkVoucherId();
        // 验证 Redis 中是否已经存在该券的秒杀活动
        /* Hash:
            1. 若用 str , 先序列化获取对象属性，进行业务操作，操作结束再反序列化回 Redis 复杂!
            2. hash 能非常容易获取库存的字段; 且在 Redis 存储过程中无需序列化与反序列化
         */
        Map<String, Object> map = redisTemplate.opsForHash().entries(key);
        AssertUtil.isTrue(!map.isEmpty() && (int) map.get("amount") > 0, "该券已经拥有了抢购活动");

        // 插入 Redis
        seckillVoucher.setIsValid(1);
        seckillVoucher.setCreateDate(now);
        seckillVoucher.setUpdateDate(now);
        redisTemplate.opsForHash().putAll(key, BeanUtil.beanToMap(seckillVoucher));
    }

    /**
     * 抢购代金券
     * 一系列健壮性处理
     * 事务回滚处理 - @Transactional
     *
     * @param voucherId   代金券 ID
     * @param accessToken 登录token
     * @Para path 访问路径
     */
    @Transactional(rollbackFor = Exception.class)
    public ResultInfo doSeckill(Integer voucherId, String accessToken, String path) {
        // 基本参数校验
        AssertUtil.isTrue(voucherId == null || voucherId < 0, "请选择需要抢购的代金券");
        AssertUtil.isNotEmpty(accessToken, "请登录");
        /* 注释原始的 关系型数据库 的流程
        // 判断此代金券是否加入抢购
        SeckillVoucher seckillVoucher = seckillVoucherMapper.selectSeckillVoucher(voucherId);
        AssertUtil.isTrue(seckillVoucher == null, "该代金券并未有抢购活动");
        // 判断是否有效
        AssertUtil.isTrue(seckillVoucher.getIsValid() == 0, "该活动已结束");
         */

        // 采用 Redis
        String key = RedisKeyConstant.seckill_voucher.getKey() + voucherId;
        Map<String, Object> map = redisTemplate.opsForHash().entries(key);
        SeckillVoucher seckillVoucher = BeanUtil.mapToBean(map, SeckillVoucher.class, true, null);

        // 判断是否开始、结束
        Date now = new Date();
        AssertUtil.isTrue(now.before(seckillVoucher.getStartTime()), "该抢购还未开始");
        AssertUtil.isTrue(now.after(seckillVoucher.getEndTime()), "该抢购已结束");
        // 判断是否卖完
        AssertUtil.isTrue(seckillVoucher.getAmount() < 1, "该券已经卖完了");
        // 获取登录用户信息
        String url = oauthServerName + "user/me?access_token={accessToken}";
        ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class, accessToken);
        if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
            resultInfo.setPath(path);
            return resultInfo;
        }
        // 这里的data是一个LinkedHashMap，SignInDinerInfo
        SignInDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(),
                new SignInDinerInfo(), false);
        // 判断登录用户是否已抢到(一个用户针对这次活动只能买一次)
        VoucherOrder order = voucherOrderMapper.findDinerOrder(dinerInfo.getId(),
                seckillVoucher.getFkVoucherId());
        AssertUtil.isTrue(order != null, "该用户已抢到该代金券，无需再抢");

        /* 注释原始的 关系型数据库 的流程
        // 扣库存; count 为修改行数，若 count = 0; 说明已售完，抢购失败
        int count = seckillVoucherMapper.stockDecrease(seckillVoucher.getId());
        AssertUtil.isTrue(count == 0, "该券已经卖完了");
         */

        // 使用 Redis 锁一个账号只能购买一次
        String lockName = RedisKeyConstant.lock_key.getKey() + dinerInfo.getId() + ":" + voucherId;
        long expireTime = seckillVoucher.getEndTime().getTime() - now.getTime();

        // 自定义 Redis 分布式锁实现
        // String lockKey = redisLock.tryLock(lockName, expireTime);
        RLock lock = redissonClient.getLock(lockName);

        try {
            // 不为空，则意味着拿到锁了，执行下单

            // 自定义 Redis 分布式锁实现
            //if (StrUtil.isNotBlank(lockKey)) {

            // Redisson 分布式锁实现
            boolean isLocked = lock.tryLock(expireTime, TimeUnit.MILLISECONDS);
            if (isLocked) {
                // 下单
                VoucherOrder voucherOrder = new VoucherOrder();
                voucherOrder.setFkDinerId(dinerInfo.getId());
                // Redis 中不需要维护外键信息
                // voucherOrder.setFkSeckillId(seckillVoucher.getId());
                voucherOrder.setFkVoucherId(seckillVoucher.getFkVoucherId());
                String orderNo = IdUtil.getSnowflake(1, 1).nextIdStr();
                voucherOrder.setOrderNo(orderNo);
                voucherOrder.setOrderType(1);
                voucherOrder.setStatus(0);
                long count = voucherOrderMapper.save(voucherOrder);
                AssertUtil.isTrue(count == 0, "用户抢购失败");    // count = 1; 说明更新了一条数据， 也即添加成功（抢购成功

                // 采用 Redis
                // 扣库存
                /*
                    问题：这里添加了 @Transactional 事务回滚，有效解决了并发情况下订单超出生成的问题；而无法解决库存扣除问题：5000并发压力测试下 amount = -3
                    分析：这里 redis 的操作为两步：对库存 amount 先查后扣，在如此高并发的前提下是容易出错的；
                    分析纠正：increment 本身是原子性的操作; 而这里：先扣库存，再判断count < 0 这两步不是原子性的，所以在高并发下才出现库存扣为负数。
                    解决1：需保证原子性：查询和扣除在一个线程中完成。 --- lua 脚本注入

                    解决2：这里只需要在 count <0 的时候把扣掉的库存再increment回去，然后再抛出异常。 测试代码如下：
                    @GetMapping("test")
                    public JsonData test() {
                        Long stock = redisTemplate.opsForValue().decrement("stock");
                        if (stock < 0) {
                            redisTemplate.opsForValue().increment("stock");
                            return JsonData.buildError("库存不足");
                        }
                        return JsonData.buildSuccess("扣库存成功");
                    }
                */

                /* 注释掉 Redis 方式
                count = redisTemplate.opsForHash().increment(key, "amount", -1);
                AssertUtil.isTrue(count < 0, "该券已经卖完了!");
                 */

                // 采用 Redis + Lua 解决问题
                // 扣库存
                List<String> keys = new ArrayList<>();
                keys.add(key);
                keys.add("amount");
                Long amount = (Long) redisTemplate.execute(defaultRedisScript, keys);
                AssertUtil.isTrue(amount == null || amount < 1, "该券已经卖完!");
            }
        } catch (Exception e) {
            // 手动回滚事务
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            // 解锁

            // 自定义 Redis 解锁实现
            // redisLock.unlock(lockName, lockKey);

            // Redisson 解锁实现
            lock.unlock();
            if (e instanceof ParameterException) {
                return ResultInfoUtil.buildError(0, "该券已经卖完了！", path);
            }
        }

        return ResultInfoUtil.buildSuccess(path, "抢购成功");
    }

}
